package com.shux.common.plugin;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;

import javax.xml.bind.PropertyException;

import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.ExecutorException;
import org.apache.ibatis.executor.statement.BaseStatementHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.scripting.xmltags.ForEachSqlNode;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.shux.utils.other.ReflectHelper;
import com.shux.utils.other.Tools;
import com.shux.utils.page.Page;

/**
 **********************************************
 *  描述：分页拦截器，在需要分页的SQL中，加入Count和分页用的limit等信息
 * Simba.Hua
 * 2017年5月21日
 **********************************************
**/
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
public class PagePlugin implements Interceptor{
    private static Logger logger = LoggerFactory.getLogger(PagePlugin.class);
    private static String dialect = "";
    private static String pageSqlId = "";
    @Override
    public Object intercept(Invocation ivk) throws Throwable {
        if(ivk.getTarget() instanceof RoutingStatementHandler){
            RoutingStatementHandler statementHandler = (RoutingStatementHandler)ivk.getTarget();
            BaseStatementHandler delegate = (BaseStatementHandler)ReflectHelper.getValueByFieldName(statementHandler, "delegate");
            MappedStatement mappedStatement = (MappedStatement)ReflectHelper.getValueByFieldName(delegate, "mappedStatement");
            
            if(mappedStatement.getId().matches(pageSqlId)){
                BoundSql boundSql = delegate.getBoundSql();
                // 分页SQL<select>中parameterType属性对应的实体参数，即Mapper接口中执行分页方法的参数,该参数不得为空
                Object parameterObject = boundSql.getParameterObject();
                if(parameterObject == null){
                    throw new NullPointerException("paramterObject 尚未实例化!");
                }else{
                    Connection connection = (Connection)ivk.getArgs()[0];
                    String sql = boundSql.getSql();
                    String origSql = sql;
                    String countSql = "select count(9) from (" + origSql +") tmp_count";
                    
                    PreparedStatement countStmt = connection.prepareStatement(countSql);
                    BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(),countSql,boundSql.getParameterMappings(),parameterObject);
                    setParameters(countStmt,mappedStatement,countBS,parameterObject);
                    ResultSet rs = countStmt.executeQuery();
                    int count = 0;
                    if(rs.next()){
                        count = rs.getInt(1);
                    }
                    rs.close();
                    countStmt.close();
                    
                    Page page = null;
                    if(parameterObject instanceof Page){
                        page = (Page)parameterObject;
                        page.setEntityOrField(true);
                        page.setTotalResult(count);
                    }else{
                        Field pageField = ReflectHelper.getFieldByFieldName(parameterObject, "page");
                        if(pageField !=null){
                            page = (Page) ReflectHelper.getValueByFieldName(parameterObject, "page");
                            if(page == null){
                                page = new Page();
                            }else{
                                throw new NoSuchFieldException(parameterObject.getClass().getName() + "不存在page属性");
                            }
                        }
                    }
                    String pageSql = generatePageSql(sql,page);
                    ReflectHelper.setValueByFieldName(boundSql, "sql", pageSql);
                }
            }
            
        }
        return ivk.proceed();
    }
    
    private String generatePageSql(String sql, Page page) {
        if(page != null && Tools.notEmpty(dialect)){
            StringBuffer pageSql = new StringBuffer();
            if("mysql".equals(dialect)){
                pageSql.append(sql)
                       .append(" limit "+page.getCurrentResult() + "," + page.getShowCount());
            }
            return pageSql.toString();
        }else{
            return sql;  
        }
       
    }

    private void setParameters(PreparedStatement ps,MappedStatement mappedStatement,BoundSql boundSql,Object parameterObject) throws SQLException{
        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if(parameterMappings != null){
            Configuration configuration = mappedStatement.getConfiguration();
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            MetaObject metaObject = parameterObject == null?null:configuration.newMetaObject(parameterObject);
            for(int i=0;i<parameterMappings.size();i++){
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if(parameterMapping.getMode() != ParameterMode.OUT){
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    PropertyTokenizer pro = new PropertyTokenizer(propertyName);
                    if(parameterObject == null){
                        value = null;
                    }else if(typeHandlerRegistry.hasTypeHandler(parameterMapping.getClass())){
                        value = parameterObject;
                    }else if(boundSql.hasAdditionalParameter(propertyName)){
                        value = boundSql.getAdditionalParameter(propertyName);
                    }else if(propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX) && boundSql.hasAdditionalParameter(pro.getName())){
                        value = boundSql.getAdditionalParameter(pro.getName());
                        if(value != null){
                            value = configuration.newMetaObject(value).getValue(propertyName.substring(pro.getName().length()));
                        }
                    }else{
                        value = metaObject == null? null:metaObject.getValue(propertyName);
                    }
                    @SuppressWarnings("unchecked")
                    TypeHandler<Object> typeHandler = (TypeHandler<Object>) parameterMapping.getTypeHandler();
                    if(typeHandler == null){
                        throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId());
                    }
                    typeHandler.setParameter(ps, i+1, value, parameterMapping.getJdbcType());
                    
                }
            }
            
        }
        
    }
    @Override
    public Object plugin(Object arg0) {
        // TODO Auto-generated method stub
        return Plugin.wrap(arg0, this);
    }

    @Override
    public void setProperties(Properties p){
       dialect = p.getProperty("dialect");
       if(Tools.isEmpty(dialect)){
           try {
            throw new PropertyException(" dialect property is not found!");
        } catch (PropertyException e) {
            logger.error(" dialect property is not found!",e);
        }
           
       }
       
       pageSqlId = p.getProperty("pageSqlId");
       if(Tools.isEmpty(pageSqlId)){
           try {
            throw new PropertyException(" pageSqlId property is not found !");
        } catch (PropertyException e) {
            logger.error("pageSqlId property is not found!",e);
        }
           
       }
    }

}
